;--------------------------
;主引导扇区程序
;Anchor: wjc133
;--------------------------

;设置常量
app_lba_start equ 100 ;用户程序在硬盘中存储位置的起始地址

;mbr段，vstart=0x7c00使计算汇编地址时，都以0x7c00为基准值进行计算
SECTION mbr align=16 vstart=0x7c00
    ;初始化
    mov ax,0
    mov ss,ax ;栈段寄存器
    mov sp,ax ;栈指针寄存器

    mov ax, [cs:phy_base] ;phy_base是用户程序加载到内存的首地址
    mov dx, [cs:phy_base+0x02]
    mov bx, 16
    div bx
    mov ds,ax ;初始化ds和es，初始值均为用户程序加载到内存后的段地址
    mov es,ax

    ; 准备过程调用的输入，这里由于app_lba_start是100，没有占满16位，所以高16位就使用0来补齐
    xor di,di 
    mov si,app_lba_start
    xor bx,bx  ;ds:bx
    call read_hard_disk_0

    ; 判断程序大小，以确定读取多少个扇区
    mov dx,[2] ;将ds:[2]位置的值放入dx，其实该步骤是将用户程序的前四个字节拿出来，这部分表示的正是用户程序的长度
    mov ax,[0] ;dx:ax表示了用户程序的长度
    mov bx,512 ;每次读取一个扇区
    div bx
    cmp dx,0 ;做除法后，ax为商，dx为余数，余数为0，说明刚好除尽。由于之前已经读取了一个扇区，所以如果除尽，则减掉一个扇区，如果没除尽则不处理
    jnz @1  ;除尽余数为0，ZF=1，jnz是ZF不为1时，也就是未除尽时跳转
    dec ax,1 ;除尽则减1

@1:
    cmp ax,0 ;商是否也为0，如果为0，则证明用户程序的长度小于512字节，不需要再读取了
    jz direct

    ;读取剩余扇区
    push ds  ;下面读取磁盘数据需要改变ds的值
    mov cx,ax ;ax即为要读取的次数，每次读取512字节
@2:
    mov ax,ds
    add ax,0x20
    mov ds,ax

    xor bx,bx
    inc si
    call read_hard_disk_0
    loop @2

    pop ds

;------------------------------------
;用户程序重定位
;包括定位入口点地址和重定位段地址
;------------------------------------
direct:
    ;重新计算用户程序入口点的段地址
    mov dx,[0x06]
    mov ax,[0x08]
    call calc_segment_base
    mov [0x06],ax

    ;处理段重定位表
    mov cx, [0x0a] ;重定位段的项数
    mov bx, 0x0c   ;下一个重定位项（首个是0x0c）

realloc:
    mov dx,[bx+0x02]
    mov ax,[bx]
    call calc_segment_base
    mov [bx],ax
    add bx,4
    loop realloc

    jmp far [0x04] ;16位间接绝对远跳转，该指令会从偏移地址处取出两个字，分别代替CS和IP的内容，高位字代表CS，低位字代表IP

;-------------------------------------
;过程调用：calc_segment_base
;输入：DX:AX=程序入口点的32位物理地址
;输出：AX=16位的段基地址
;-------------------------------------
calc_segment_base:
    ;需要暂存起来防止破坏的通用寄存器值，这些值可以在过程中使用到时填写到此处
    push dx
    ;重新计算用户程序被加载后的物理地址，实际上就是将程序原有的地址加上加载时的初始地址(phy_base)
    add ax,[phy_base]
    adc dx,[phy_base+0x20] ;带进位的加法，上一步若产生进位，这一步就一并加进来
    ;物理地址右移四位即可获得段地址，32位分别在两个寄存器中，需要分别右移
    shr ax,4
    ror dx,4
    and dx,0xf000
    or ax,dx

    pop dx
    ret

;-------------------------------------
;过程调用：read_hard_disk_0
;过程调用类似于高级语言中的函数
;
;输入: DI:SI=起始逻辑区扇号
;     DS:BX=目标缓冲区地址
;-------------------------------------
read_hard_disk_0:
    ;暂存通用寄存器的值，待调用结束再倒序恢复
    ;该过程类似函数调用，函数调用过程中值的改变不影响调用方
    ;因此，此处我们可以大胆地使用通用寄存器了
    push ax
    push bx
    push cx
    push dx

    ;读硬盘
    mov dx,0x1f2 ;0x1f2是一个8位端口，表示要读取的扇区数
    mov al,1
    out dx,al  ;out还是in，是相对于CPU来说的，此处是向dx表示的端口输出数据

    inc dx
    mov ax,si  ;0-7位
    out dx,al

    inc dx
    mov al,ah  ;8-15位
    out dx,al

    inc dx
    mov ax,di  ;16-23位
    out dx,al

    inc dx
    mov al,0xe0
    or al,ah
    out dx,al

    inc dx
    mov al,0x20 ;读模式
    out dx,al

.waits:
    in al,dx
    and al,0x88 ;0x10001000，即只关心BSY位和DRQ位
    cmp al,0x08
    jnz .waits

    mov cx,256 ;要读取的字数，也即512个字节
    mov dx,0x1f0
.readw:
    in ax,dx  ;从0x1f0这个16位端口读数据
    mov [bx],ax ;bx是用户程序缓冲到内存上的偏移地址
    add bx,2
    loop .readw

    pop dx
    pop cx
    pop bx
    pop ax

    ret  ;回到调用方的位置
;--------------------------------------------
phy_base dd 0x10000

times 510-($-$$) db 0
                db 0x55,0xaa